Skip to content

feat(web): pull the four new design-system primitives into the app (HT-93) - #103

Merged
zaridan merged 2 commits into
mainfrom
feat/ht-93-ds-new-primitives
Jul 20, 2026
Merged

feat(web): pull the four new design-system primitives into the app (HT-93)#103
zaridan merged 2 commits into
mainfrom
feat/ht-93-ds-new-primitives

Conversation

@zaridan

@zaridan zaridan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Closes HT-93.

Why

The Claude Design "Helpthread" project gained four net-new primitives on 2026-07-19, authored in response to a request from app work. They were never pulled down. They were sitting in templates/new-primitives/Primitives.jsx, unused.

Primitive Maps to
SplitButton Send + "Send and close" / "Send and snooze"
CommandMenu Saved-replies inserter (saved_replies table)
SnoozePicker Presets + calendar (snooze-wake cron)
CredentialRow / PasskeyList Passkey management (webauthn_credentials, HT-75)

The conversion

Mechanical, with every style value preserved. The design source is browser-rendered, so it used React.createElement, module.exports, and local duplicate copies of MenuItem/Button. Those become JSX, ESM exports, and real imports from ./MenuItem and ./Button.

Dropped: the Showcase, its layout scaffolding (Section/Panel/StateLabel/Stack/twoUp), and the specimen fixtures. Design-project demo code, not app code.

Shared helpers (icon glyphs, the RING token, date formatters) go in primitives-support.jsx — the source carried one copy in a single file, and splitting that file must not turn one definition into four.

One intentional behavioral difference

The design source froze a reference clock (NOW = 2026-07-19 14:30) so specimen times wouldn't drift between renders. Correct for a showcase, wrong for the appSnoozePicker would compute "tomorrow" from a date in the past. Replaced with now(), commented at the site.

Five lint findings suppressed, not fixed

Biome marks most of these FIXABLE, but each "fix" would be silent drift from the approved design:

  • noAutofocus ×2 — both fields appear only in response to an explicit user action (opening the command menu; clicking Rename). Removing autofocus would strand keyboard users.
  • noArrayIndexKey ×2 — weekday initials repeat (two T, two S) in a fixed-length row that never reorders; month-padding cells have no identity of their own.
  • useExhaustiveDependencies ×1q is the effect's trigger, not a read. Dropping it would stop the highlight resetting when the query changes.

Each carries a biome-ignore with the reason inline.

Verification

Check Result
biome check web/src/components/ds/core/ exit 0
tsc -p tsconfig.json exit 0
next build exit 0

None of the existing 16 ds/ components were modifiedgit status shows only the 10 new files. The formatter pass was run with --linter-enabled=false specifically so it couldn't touch them or apply behavior-changing lint fixes.

Not in this PR

  • Wiring these into features — separate tickets. They compile and are ready; nothing imports them yet.
  • Promoting them to components/core/ upstream so the design project's own library matches. Right now they live in templates/ remotely, so they're staged on both sides.
  • The Biome-formatting problem. web/src/components/ds/ is Biome-formatted, so it no longer byte-matches the design project despite CLAUDE.md requiring verbatim copies. Button.jsx was confirmed semantically identical — the entire diff is quotes, semicolons, and wrapping. This makes byte comparison useless as a drift detector and wants a deliberate call: exclude ds/ from Biome, or adopt a normalizing comparison. Filed in HT-93's description.

Reviewer attention

The claim worth checking is pixel fidelity — that no style value drifted in the createElement → JSX conversion. Worth a spot-check against templates/new-primitives/Primitives.jsx in the design project, particularly SplitButton's seam/border handling and SnoozePicker's MiniCalendar cell states, which had the densest conditional styling.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added searchable, keyboard-navigable saved-reply menus.
    • Added passkey displays with rename, revoke confirmation, and add-passkey actions.
    • Added snooze controls with presets or custom date and time selection.
    • Added split buttons with loading, disabled, and dropdown action states.
    • Added shared icons, focus handling, and date/time formatting support.

…T-93)

The Claude Design "Helpthread" project gained four net-new primitives on
2026-07-19, authored in response to a request from app work. They were
never pulled down. This adds them to web/src/components/ds/core/:

- SplitButton — primary action with an attached caret menu
- CommandMenu — searchable saved-replies inserter
- SnoozePicker — presets plus a custom calendar and time
- CredentialRow / PasskeyList — passkey management, rename and two-step revoke

Shared icon glyphs, the focus-ring token, and the date formatters live in
primitives-support.jsx rather than being duplicated per component — the
design source carried one copy in a single file, and splitting that file
must not turn one definition into four.

Conversion from the design source was mechanical and every style value is
preserved: the source is browser-rendered, so it used React.createElement,
module.exports, and local duplicate copies of MenuItem/Button. Those become
JSX, ESM exports, and real imports from ./MenuItem and ./Button. The
Showcase, its layout scaffolding, and the specimen fixtures are dropped —
they are design-project demo code, not app code.

One intentional behavioral difference, commented at the site: the design
source froze a reference clock (NOW = 2026-07-19 14:30) so specimen times
would not drift between renders. The app uses real time via now(); a frozen
clock would make SnoozePicker compute "tomorrow" from a past date.

Five lint findings are suppressed with reasons rather than auto-fixed,
because Biome's fixes would each be silent behavior drift from the design:
two noAutofocus (both fields appear only in response to an explicit user
action), two noArrayIndexKey (repeating weekday initials in a fixed row,
and month-padding cells that have no identity), and one
useExhaustiveDependencies (q is the effect's trigger, not a read).

Verified: biome check exit 0, tsc exit 0, next build exit 0. None of the
existing 16 ds components were modified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7bd8570e-2203-4113-a9a8-ca69cc74333f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds shared design-system primitives and declarations alongside four interactive React components: a searchable command menu, passkey credential controls, a snooze picker, and a split action button.

Changes

Design system components

Layer / File(s) Summary
Shared icons, focus, and date utilities
web/src/components/ds/core/primitives-support.*
Adds reusable SVG icons, focus-ring state handling, date constants, timestamp formatting, and TypeScript declarations.
Searchable command menu
web/src/components/ds/core/CommandMenu.*
Adds query filtering, keyboard navigation, highlighted-item scrolling, empty states, and reply selection.
Snooze date and time picker
web/src/components/ds/core/SnoozePicker.*
Adds preset snooze times, custom calendar/time selection, wake-time resolution, and snooze submission.
Credential and split-action controls
web/src/components/ds/core/CredentialRow.*, web/src/components/ds/core/SplitButton.*
Adds passkey rename/revoke/list interactions and a loading-aware split button with selectable menu actions.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding four design-system primitives, though "into the app" is slightly broad.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ht-93-ds-new-primitives

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
web/src/components/ds/core/primitives-support.d.ts (1)

2-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer React.JSX.Element over the deprecated global JSX.Element namespace.

React 19 removed the global JSX namespace in favor of React.JSX. While @types/react@19 still ships a backwards-compatible global for now, it is deprecated and will be removed in a future release. Since this file doesn't import React, add a type-only import and update the return types.

♻️ Proposed refactor
+import type { JSX } from 'react'
+
 export declare const RING: string
-export declare function chevron(dir?: 'down' | 'up' | 'left' | 'right', sz?: number): JSX.Element
-export declare function IconKey(sz?: number): JSX.Element
-export declare function IconSearch(sz?: number): JSX.Element
-export declare function IconReply(sz?: number): JSX.Element
-export declare function IconClock(sz?: number): JSX.Element
-export declare function IconPlus(sz?: number): JSX.Element
-export declare function IconPencil(sz?: number): JSX.Element
-export declare function IconTrash(sz?: number): JSX.Element
+export declare function chevron(dir?: 'down' | 'up' | 'left' | 'right', sz?: number): JSX.Element
+export declare function IconKey(sz?: number): JSX.Element
+export declare function IconSearch(sz?: number): JSX.Element
+export declare function IconReply(sz?: number): JSX.Element
+export declare function IconClock(sz?: number): JSX.Element
+export declare function IconPlus(sz?: number): JSX.Element
+export declare function IconPencil(sz?: number): JSX.Element
+export declare function IconTrash(sz?: number): JSX.Element

With import type { JSX } from 'react', the JSX.Element references resolve to React.JSX.Element without needing the global namespace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/primitives-support.d.ts` around lines 2 - 9, Add a
type-only React import in the declarations file and update the return types of
chevron, IconKey, IconSearch, IconReply, IconClock, IconPlus, IconPencil, and
IconTrash to use the imported React JSX element type instead of the deprecated
global JSX.Element.
web/src/components/ds/core/SplitButton.jsx (1)

99-122: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Caret toggle lacks aria-expanded/aria-haspopup.

The caret button controls the dropdown (open state) but doesn't expose aria-haspopup="menu"/aria-expanded={open} for assistive tech. Since ds/core files must remain verbatim copies of the design source, please confirm whether the prototype already includes these attributes; if not, this should be added upstream first rather than patched locally.

As per coding guidelines, "Design-system files under web/src/components/ds/ must remain verbatim copies of the Claude Design prototype/design-system source; improvements must be made upstream in the design project first."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/SplitButton.jsx` around lines 99 - 122, Verify
whether the upstream design-system prototype’s caret button includes
aria-haspopup="menu" and aria-expanded={open}; if present, synchronize this
SplitButton caret button with the prototype. If absent, make no local change and
instead flag the upstream design source for adding these attributes before
updating the verbatim copy.

Source: Coding guidelines

web/src/components/ds/core/CredentialRow.jsx (1)

290-290: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

key={c.name} assumes credential names are unique.

Credential has no id field, and renaming is user-controlled, so two credentials could end up sharing a name, causing React key collisions/incorrect reconciliation. Consider a stable id on Credential for the list key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/CredentialRow.jsx` at line 290, Update the
credential list rendering around CredentialRow to use a stable, unique
Credential identifier for the React key instead of c.name. Add or propagate an
id field on Credential as needed, and pass that identifier to key while
preserving the existing row props and ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/components/ds/core/CommandMenu.d.ts`:
- Line 15: Update the CommandMenu declaration to import the JSX type namespace
from React and retain JSX.Element as the return type of CommandMenu, removing
its reliance on the ambient global JSX namespace.

In `@web/src/components/ds/core/CommandMenu.jsx`:
- Around line 47-49: Update the ArrowDown handling in the CommandMenu highlight
state update to clamp the highlighted index at zero when filtered is empty,
while preserving the existing upper bound for non-empty results. Ensure hi
remains non-negative so later-arriving items can be highlighted and selected.

In `@web/src/components/ds/core/CredentialRow.jsx`:
- Around line 230-291: Update PasskeyList to accept onRename and onRevoke and
forward both callbacks to every rendered CredentialRow, preserving the
credential and new name arguments used by CredentialRow. Add the corresponding
optional callback declarations to PasskeyListProps in CredentialRow.d.ts.

In `@web/src/components/ds/core/SnoozePicker.jsx`:
- Around line 169-174: Update the resolved useMemo in SnoozePicker to validate
that time is non-empty and contains valid hour/minute values before calling
setHours; return a safe non-submittable state when invalid. Ensure the
confirmation display does not use an invalid Date, and disable or guard the
submission path so onSnooze cannot receive an invalid resolved value.
- Around line 5-8: Update laterToday() so it never returns a past timestamp:
after 5 PM, either hide the Later today preset or roll its timestamp forward to
the next valid slot, while preserving the existing 5 PM behavior before that
cutoff.

---

Nitpick comments:
In `@web/src/components/ds/core/CredentialRow.jsx`:
- Line 290: Update the credential list rendering around CredentialRow to use a
stable, unique Credential identifier for the React key instead of c.name. Add or
propagate an id field on Credential as needed, and pass that identifier to key
while preserving the existing row props and ordering.

In `@web/src/components/ds/core/primitives-support.d.ts`:
- Around line 2-9: Add a type-only React import in the declarations file and
update the return types of chevron, IconKey, IconSearch, IconReply, IconClock,
IconPlus, IconPencil, and IconTrash to use the imported React JSX element type
instead of the deprecated global JSX.Element.

In `@web/src/components/ds/core/SplitButton.jsx`:
- Around line 99-122: Verify whether the upstream design-system prototype’s
caret button includes aria-haspopup="menu" and aria-expanded={open}; if present,
synchronize this SplitButton caret button with the prototype. If absent, make no
local change and instead flag the upstream design source for adding these
attributes before updating the verbatim copy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b30c6b5f-14f6-40b1-a7be-a6a7afa78542

📥 Commits

Reviewing files that changed from the base of the PR and between aca83cb and b5afa9a.

📒 Files selected for processing (10)
  • web/src/components/ds/core/CommandMenu.d.ts
  • web/src/components/ds/core/CommandMenu.jsx
  • web/src/components/ds/core/CredentialRow.d.ts
  • web/src/components/ds/core/CredentialRow.jsx
  • web/src/components/ds/core/SnoozePicker.d.ts
  • web/src/components/ds/core/SnoozePicker.jsx
  • web/src/components/ds/core/SplitButton.d.ts
  • web/src/components/ds/core/SplitButton.jsx
  • web/src/components/ds/core/primitives-support.d.ts
  • web/src/components/ds/core/primitives-support.jsx

width?: number
initialQuery?: string
}
export declare function CommandMenu(props: CommandMenuProps): JSX.Element

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does @types/react19.2.4 expose a global JSX namespace, or should library declaration files import JSX fromreactand useJSX.Element?

💡 Result:

No, @types/react 19.2.4 does not expose a global JSX namespace [1][2]. The global JSX namespace was removed in @types/react version 19 to prevent type pollution and conflicts with other UI libraries that also utilize JSX [3][4][1]. For library declaration files and general TypeScript code, you should import the JSX namespace directly from React and use React.JSX.Element instead of the previously global JSX.Element [1][5]. You can access it by importing: import { JSX } from 'react'; // Use as const myElement: JSX.Element =

; If you need to perform module augmentation to add intrinsic elements, you can no longer use declare global [6][5]. Instead, you must target the specific React module based on your compiler's JSX runtime setting [3][4][6]: declare module 'react' { namespace JSX { interface IntrinsicElements { 'my-element': { myElementProps: string }; } } } The exact module name to augment depends on your tsconfig.json configuration [3][4]: - For "jsx": "react-jsx", use 'react/jsx-runtime'. - For "jsx": "react-jsxdev", use 'react/jsx-dev-runtime'. - For "jsx": "react" or "preserve", use 'react'.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='web/src/components/ds/core/CommandMenu.d.ts'

echo '--- file outline ---'
wc -l "$file"
echo
cat -n "$file"

echo
echo '--- search for JSX imports/usages in nearby ds core declarations ---'
rg -n --hidden --glob 'web/src/components/ds/core/*.{d.ts,ts,tsx,js,jsx}' 'from '\''react'\''|namespace JSX|JSX\.Element|import type \{ JSX \}' .

Repository: Helpthread/helpthread

Length of output: 4929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package.json ---'
if [ -f package.json ]; then
  cat -n package.json | sed -n '1,240p'
fi

echo
echo '--- tsconfig files mentioning jsx or types ---'
for f in $(git ls-files '*tsconfig*.json' 'package.json' 'pnpm-lock.yaml' 'yarn.lock' 'package-lock.json' 2>/dev/null); do
  case "$f" in
    *tsconfig*.json|package.json)
      echo "### $f"
      cat -n "$f" | sed -n '1,240p'
      echo
      ;;
  esac
done

echo '--- any JSX namespace declarations in repo ---'
rg -n --hidden --glob '!**/node_modules/**' 'declare\s+namespace\s+JSX|namespace\s+JSX|interface\s+IntrinsicElements' .

Repository: Helpthread/helpthread

Length of output: 4990


Reference React’s scoped JSX namespace.
web/src/components/ds/core/CommandMenu.d.ts:15 should not depend on the ambient global JSX namespace. Import type { JSX } from 'react' and keep the return type as JSX.Element so this public declaration stays compatible with React 19 consumers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/CommandMenu.d.ts` at line 15, Update the
CommandMenu declaration to import the JSX type namespace from React and retain
JSX.Element as the return type of CommandMenu, removing its reliance on the
ambient global JSX namespace.

Comment on lines +47 to +49
if (e.key === 'ArrowDown') {
e.preventDefault()
setHi((i) => Math.min(i + 1, filtered.length - 1))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the highlighted index non-negative for empty results.

Line 49 sets hi to -1 when filtered is empty. If items later arrive without changing q, the menu has no highlighted row and Enter cannot select one.

Proposed fix
-      setHi((i) => Math.min(i + 1, filtered.length - 1))
+      setHi((i) => Math.max(0, Math.min(i + 1, filtered.length - 1)))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (e.key === 'ArrowDown') {
e.preventDefault()
setHi((i) => Math.min(i + 1, filtered.length - 1))
if (e.key === 'ArrowDown') {
e.preventDefault()
setHi((i) => Math.max(0, Math.min(i + 1, filtered.length - 1)))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/CommandMenu.jsx` around lines 47 - 49, Update the
ArrowDown handling in the CommandMenu highlight state update to clamp the
highlighted index at zero when filtered is empty, while preserving the existing
upper bound for non-empty results. Ensure hi remains non-negative so
later-arriving items can be highlighted and selected.

Comment on lines +230 to +291
export function PasskeyList({ creds = [], empty, onAdd }) {
const addBtn = (
<Button variant="outline" onClick={() => onAdd?.()}>
<span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span>
Add a passkey
</Button>
)
return (
<div
style={{
border: '1px solid var(--ht-divider)',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface)',
overflow: 'hidden',
}}
>
{empty || creds.length === 0 ? (
<div style={{ padding: '40px 24px 34px', textAlign: 'center' }}>
<div
style={{
width: 44,
height: 44,
margin: '0 auto 14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface-2)',
color: 'var(--ht-ink-dim)',
}}
>
{IconKey(24)}
</div>
<div
style={{
fontFamily: 'var(--ht-display)',
fontSize: 18,
fontWeight: 600,
color: 'var(--ht-ink)',
}}
>
No passkeys yet
</div>
<div
style={{
margin: '7px auto 18px',
maxWidth: 320,
fontSize: 13.5,
lineHeight: 1.6,
color: 'var(--ht-ink-muted)',
}}
>
Add a passkey to sign in with your fingerprint, face, or security key — no password to
remember.
</div>
{addBtn}
</div>
) : (
<>
{creds.map((c, i) => (
<CredentialRow key={c.name} cred={c} first={i === 0} />
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

PasskeyList never forwards onRename/onRevoke to CredentialRow.

PasskeyList only destructures creds, empty, onAdd. Each rendered CredentialRow therefore always has onRename/onRevoke as undefined, so the rename/revoke UI is fully interactive but has no observable effect — onRename?.(cred, name) and onRevoke?.(cred) are silent no-ops for anyone using the composed PasskeyList (rather than CredentialRow directly). This defeats the primary purpose of the list.

🐛 Proposed fix
-export function PasskeyList({ creds = [], empty, onAdd }) {
+export function PasskeyList({ creds = [], empty, onAdd, onRename, onRevoke }) {
   ...
   {creds.map((c, i) => (
-    <CredentialRow key={c.name} cred={c} first={i === 0} />
+    <CredentialRow key={c.name} cred={c} first={i === 0} onRename={onRename} onRevoke={onRevoke} />
   ))}

(Also add onRename?: (cred: Credential, name: string) => void and onRevoke?: (cred: Credential) => void to PasskeyListProps in CredentialRow.d.ts.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function PasskeyList({ creds = [], empty, onAdd }) {
const addBtn = (
<Button variant="outline" onClick={() => onAdd?.()}>
<span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span>
Add a passkey
</Button>
)
return (
<div
style={{
border: '1px solid var(--ht-divider)',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface)',
overflow: 'hidden',
}}
>
{empty || creds.length === 0 ? (
<div style={{ padding: '40px 24px 34px', textAlign: 'center' }}>
<div
style={{
width: 44,
height: 44,
margin: '0 auto 14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface-2)',
color: 'var(--ht-ink-dim)',
}}
>
{IconKey(24)}
</div>
<div
style={{
fontFamily: 'var(--ht-display)',
fontSize: 18,
fontWeight: 600,
color: 'var(--ht-ink)',
}}
>
No passkeys yet
</div>
<div
style={{
margin: '7px auto 18px',
maxWidth: 320,
fontSize: 13.5,
lineHeight: 1.6,
color: 'var(--ht-ink-muted)',
}}
>
Add a passkey to sign in with your fingerprint, face, or security key no password to
remember.
</div>
{addBtn}
</div>
) : (
<>
{creds.map((c, i) => (
<CredentialRow key={c.name} cred={c} first={i === 0} />
))}
export function PasskeyList({ creds = [], empty, onAdd, onRename, onRevoke }) {
const addBtn = (
<Button variant="outline" onClick={() => onAdd?.()}>
<span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span>
Add a passkey
</Button>
)
return (
<div
style={{
border: '1px solid var(--ht-divider)',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface)',
overflow: 'hidden',
}}
>
{empty || creds.length === 0 ? (
<div style={{ padding: '40px 24px 34px', textAlign: 'center' }}>
<div
style={{
width: 44,
height: 44,
margin: '0 auto 14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'var(--ht-radius-md)',
background: 'var(--ht-surface-2)',
color: 'var(--ht-ink-dim)',
}}
>
{IconKey(24)}
</div>
<div
style={{
fontFamily: 'var(--ht-display)',
fontSize: 18,
fontWeight: 600,
color: 'var(--ht-ink)',
}}
>
No passkeys yet
</div>
<div
style={{
margin: '7px auto 18px',
maxWidth: 320,
fontSize: 13.5,
lineHeight: 1.6,
color: 'var(--ht-ink-muted)',
}}
>
Add a passkey to sign in with your fingerprint, face, or security key no password to
remember.
</div>
{addBtn}
</div>
) : (
<>
{creds.map((c, i) => (
<CredentialRow key={c.name} cred={c} first={i === 0} onRename={onRename} onRevoke={onRevoke} />
))}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/CredentialRow.jsx` around lines 230 - 291, Update
PasskeyList to accept onRename and onRevoke and forward both callbacks to every
rendered CredentialRow, preserving the credential and new name arguments used by
CredentialRow. Add the corresponding optional callback declarations to
PasskeyListProps in CredentialRow.d.ts.

Comment on lines +5 to +8
function laterToday() {
const d = now()
d.setHours(17, 0, 0, 0)
return d

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file listing ==\n'
git ls-files 'web/src/components/ds/core/SnoozePicker.jsx'

printf '\n== line count ==\n'
wc -l web/src/components/ds/core/SnoozePicker.jsx

printf '\n== relevant excerpt ==\n'
cat -n web/src/components/ds/core/SnoozePicker.jsx | sed -n '1,220p'

printf '\n== search for now() / laterToday / time handling in nearby files ==\n'
rg -n "laterToday|now\(\)|type=\"time\"|setHours\(|SnoozePicker" web/src/components/ds/core web/src/components/ds -g '!**/node_modules/**'

Repository: Helpthread/helpthread

Length of output: 12578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' web/src/components/ds/core/SnoozePicker.jsx | nl -ba

Repository: Helpthread/helpthread

Length of output: 199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== SnoozePicker time section ==\n'
sed -n '240,300p' web/src/components/ds/core/SnoozePicker.jsx | cat -n

printf '\n== primitives-support clock commentary ==\n'
sed -n '80,120p' web/src/components/ds/core/primitives-support.jsx | cat -n

printf '\n== search for empty-time handling ==\n'
rg -n "time.*''|setTime\(|onChange=.*time|type=\"time\"" web/src/components/ds/core/SnoozePicker.jsx

Repository: Helpthread/helpthread

Length of output: 4323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== SnoozePicker around time input ==\n'
sed -n '248,292p' web/src/components/ds/core/SnoozePicker.jsx | cat -n

printf '\n== primitives-support around now() commentary ==\n'
sed -n '96,118p' web/src/components/ds/core/primitives-support.jsx | cat -n

Repository: Helpthread/helpthread

Length of output: 2941


Make Later today future-safe (web/src/components/ds/core/SnoozePicker.jsx:5-8). After 5 PM, laterToday() resolves to a past timestamp; hide the preset once it expires or roll it forward to the next valid slot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/SnoozePicker.jsx` around lines 5 - 8, Update
laterToday() so it never returns a past timestamp: after 5 PM, either hide the
Later today preset or roll its timestamp forward to the next valid slot, while
preserving the existing 5 PM behavior before that cutoff.

Source: Coding guidelines

Comment on lines +169 to +174
const resolved = React.useMemo(() => {
const [h, mm] = time.split(':').map(Number)
const d = new Date(sel)
d.setHours(h, mm, 0, 0)
return d
}, [sel, time])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent an invalid Date when the time field is cleared.

A native time input can emit ''; destructuring then leaves mm undefined, so setHours() produces an invalid Date. The confirmation text becomes invalid and onSnooze can receive that invalid value. Validate the time before deriving resolved and disable or guard submission until it is valid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ds/core/SnoozePicker.jsx` around lines 169 - 174, Update
the resolved useMemo in SnoozePicker to validate that time is non-empty and
contains valid hour/minute values before calling setHours; return a safe
non-submittable state when invalid. Ensure the confirmation display does not use
an invalid Date, and disable or guard the submission path so onSnooze cannot
receive an invalid resolved value.

CLAUDE.md requires web/src/components/ds/ to be verbatim copies of the
Claude Design project's components. They aren't: Biome reformats them on
arrival — the repo's javascript.formatter settings (single quotes,
semicolons asNeeded) rewrite the design source's double quotes and
semicolons, and organizeImports reorders their imports.

The code behaves identically; Button.jsx was confirmed semantically
identical to the design project's copy, with the entire diff being quotes,
semicolons and line wrapping. But that noise makes byte comparison useless
as a drift detector — a real design change would be indistinguishable from
formatter churn.

An override for ds/** already existed, disabling several lint rules that
don't suit design-source code. It left the formatter and the import assist
enabled, which is what does the rewriting. This turns both off for that
path.

Verified by construction: a file written in design-source style (double
quotes, semicolons) passes `biome check` with exit 0 inside ds/, and fails
with a format error outside it. Whole-repo `biome check` stays exit 0.

Note this prevents FUTURE mangling; it does not retroactively restore the
16 existing components, which are already reformatted. Making byte
comparison actually work requires re-pulling those from the design project
— tracked separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 20, 2026
The four new primitives (plus their shared helpers) were promoted into the
design project's components/core/ in this ticket. This re-pulls them back
down verbatim, so ds/ no longer holds five files that were a conversion
rather than a copy — every file under ds/ is now a byte-for-byte copy of its
design-project counterpart, which is what CLAUDE.md claims.

Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against
the exact bytes uploaded, all identical.

Two defects were caught by the normalized-diff pass and fixed upstream first,
then re-pulled:

- CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight
  quotes. That is rendered output, so it is a fidelity break, not formatting.
- CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the
  null the component actually branches on.

Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply
and IconClock take a required `sz`, because unlike IconSearch and IconPlus
they carry no default and render wrong without it.

The biome.json override gains noAutofocus, useExhaustiveDependencies and
noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments;
those are app-lint artifacts and do not belong in verbatim design source, so
the suppression moves to the override that already exists for exactly this
purpose. The design rationale comments stay in the components. No rule is
relaxed outside web/src/components/ds/**.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.
@zaridan
zaridan merged commit b6a1845 into main Jul 20, 2026
5 checks passed
zaridan added a commit that referenced this pull request Jul 20, 2026
PR #103 stopped Biome from reformatting web/src/components/ds/ on arrival,
but it could not un-mangle the 16 components already there. This does that:
each file re-fetched from the Claude Design "Helpthread" project via
DesignSync and written byte-for-byte — double quotes, semicolons, original
import order and line wrapping restored.

Equivalence was proven before overwriting, not assumed. Both the pre-change
and post-change trees were formatted through one canonical Biome config and
diffed; the diff is empty across all 32 files. So no style value, prop, or
branch of logic changed — the whole historical drift really was formatting,
and nothing had been hand-edited in the app or moved in the design project.

Byte fidelity spot-checked against fresh get_file responses; all 32 files
end with a newline and none carry CRLF.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.

Stacked on feat/ht-93-ds-new-primitives — the biome.json override there is a
precondition for the biome gate to pass on verbatim files.
zaridan added a commit that referenced this pull request Jul 20, 2026
The four new primitives (plus their shared helpers) were promoted into the
design project's components/core/ in this ticket. This re-pulls them back
down verbatim, so ds/ no longer holds five files that were a conversion
rather than a copy — every file under ds/ is now a byte-for-byte copy of its
design-project counterpart, which is what CLAUDE.md claims.

Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against
the exact bytes uploaded, all identical.

Two defects were caught by the normalized-diff pass and fixed upstream first,
then re-pulled:

- CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight
  quotes. That is rendered output, so it is a fidelity break, not formatting.
- CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the
  null the component actually branches on.

Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply
and IconClock take a required `sz`, because unlike IconSearch and IconPlus
they carry no default and render wrong without it.

The biome.json override gains noAutofocus, useExhaustiveDependencies and
noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments;
those are app-lint artifacts and do not belong in verbatim design source, so
the suppression moves to the override that already exists for exactly this
purpose. The design rationale comments stay in the components. No rule is
relaxed outside web/src/components/ds/**.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.
zaridan added a commit that referenced this pull request Jul 20, 2026
* chore(web): re-pull the 16 design-system components verbatim (HT-94)

PR #103 stopped Biome from reformatting web/src/components/ds/ on arrival,
but it could not un-mangle the 16 components already there. This does that:
each file re-fetched from the Claude Design "Helpthread" project via
DesignSync and written byte-for-byte — double quotes, semicolons, original
import order and line wrapping restored.

Equivalence was proven before overwriting, not assumed. Both the pre-change
and post-change trees were formatted through one canonical Biome config and
diffed; the diff is empty across all 32 files. So no style value, prop, or
branch of logic changed — the whole historical drift really was formatting,
and nothing had been hand-edited in the app or moved in the design project.

Byte fidelity spot-checked against fresh get_file responses; all 32 files
end with a newline and none carry CRLF.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.

Stacked on feat/ht-93-ds-new-primitives — the biome.json override there is a
precondition for the biome gate to pass on verbatim files.

* docs: UI fidelity is bidirectional, not design-first-only (HT-94)

The section said improvements go upstream in the design project first. That
has not been the working policy: HT-54's screens were built app-first and
approved in the app, and TJ's call (2026-07-20) is that approval in the app
is approval — the work flows back up.

Documents both directions, why ds/ is excluded from Biome (a formatter pass
breaks byte comparison as a drift detector), and that a semantic difference
found during a re-pull is a finding to escalate rather than something to
quietly resolve.

* chore(web): close the loop on the five promoted primitives (HT-94)

The four new primitives (plus their shared helpers) were promoted into the
design project's components/core/ in this ticket. This re-pulls them back
down verbatim, so ds/ no longer holds five files that were a conversion
rather than a copy — every file under ds/ is now a byte-for-byte copy of its
design-project counterpart, which is what CLAUDE.md claims.

Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against
the exact bytes uploaded, all identical.

Two defects were caught by the normalized-diff pass and fixed upstream first,
then re-pulled:

- CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight
  quotes. That is rendered output, so it is a fidelity break, not formatting.
- CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the
  null the component actually branches on.

Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply
and IconClock take a required `sz`, because unlike IconSearch and IconPlus
they carry no default and render wrong without it.

The biome.json override gains noAutofocus, useExhaustiveDependencies and
noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments;
those are app-lint artifacts and do not belong in verbatim design source, so
the suppression moves to the override that already exists for exactly this
purpose. The design rationale comments stay in the components. No rule is
relaxed outside web/src/components/ds/**.

Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.

* docs: record the design project's three-folder component taxonomy (HT-94)

TJ approved components/app/ as the home for app-level surface upstream, so
the rule it implies gets written down rather than living in one PR thread.

core/ is primitives, inbox/ is inbox-specific composition, app/ is whole
screens plus the chrome framing them; the test for app/ is that the thing
owns a route or wraps all of them. Also records the one place the two sides
are deliberately NOT byte-identical: the app/ screens are .tsx here and are
converted to presentational .jsx on the way up, unlike ds/ which is a copy.

The same taxonomy note is now in the design project's own readme.md.
zaridan added a commit that referenced this pull request Aug 2, 2026
…dules match the desk (HT-95)

Modules are out-of-process and render their own UI, so nothing today makes a
module look like the desk it installs into. web/src/components/ds/ and
web/src/theme/tokens/ are the right raw material but are AGPL, and a
born-proprietary paid module importing them links AGPL code in-process — the
case the §7 Module API Exception covers, which is still DRAFT.

Proposes publishing the pack under a permissive license as its own package:
needs no §7 exception at all, and paid → free is the permitted direction under
catalog.md §1. Components are not the moat.

Also: theming resolves against the installed desk (white-labeling is a paid
item), the pack is generated from ds/ rather than forked (same discipline as
CLAUDE.md's UI-fidelity rule, one hop out), and conformance is a marketplace
listing requirement since no runtime check can ever enforce it.

Docs only. ds/ is owned by HT-93 (PR #103) and HT-94 — untouched here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant